package pt.ist.meic.pava;

import java.util.ArrayList;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.Loader;
import javassist.NotFoundException;
import javassist.Translator;
import javassist.expr.ExprEditor;
import javassist.expr.MethodCall;

class TraitTranslator implements Translator {

	private ClassPool traitSystemPool;
	private Loader traitSystemLoader;
	private TraitTools tools;
	private ArrayList<LoadedTrait> loadedTraits;
	private String mainClass;
	private ArrayList<String> passedTraits;

	public TraitTranslator(ClassPool p) {

		this.traitSystemPool = p;
		this.traitSystemLoader = new Loader(p);
		this.tools = new TraitTools();
		this.loadedTraits = new ArrayList<LoadedTrait>();
		this.passedTraits = new ArrayList<String>();

		try {
			this.traitSystemLoader.addTranslator(p, this);
		} catch (NotFoundException e) {
			e.printStackTrace();
		} catch (CannotCompileException e) {
			e.printStackTrace();
		}

	}

	/**
	 * For more details about this method see the interface javassist.Translator
	 **/
	public void start(ClassPool pool) throws NotFoundException,
			CannotCompileException {
		/** Do nothing **/
	}

	/**
	 * If the trait requirements are not fulfilled the program will exit with
	 * code 1 For more details about this method see the interface
	 * javassist.Translator
	 **/
	public void onLoad(ClassPool pool, String className) {

		CtClass loadedClass;
		try {

			loadedClass = pool.get(className);

			if (!loadedClass.isInterface()) {

				this.loadedTraits.clear();
				this.passedTraits.clear();
				this.mainClass = loadedClass.getName();

				/** Start composing Traits **/
				LoadedTrait m = new LoadedTrait(loadedClass);

				m = composeTrait(m);

				// TODO verificar se isto é necessário
				ArrayList<TraitMethod> implementedMethodsList = m
						.getImplementedMethods();
				ArrayList<CtMethod> requiredMethodsList = m
						.getRequiredMethods();

				// TODO criar exception para conflit
				/** Check conflits **/
				if (!m.getConflits().isEmpty()) {

					for (Conflit conflit : m.getConflits()) {

						System.err.println("Conflit on " + conflit.getName()
								+ " method on Traits:");

						for (String a : conflit.getTraits())

							System.err.println("\t" + a);
					}

					System.exit(1);
				}

				// TODO Check if exists requiredMethods (superclass)
				// TODO criar exception para required
				/** Check if exists requiredMethods **/
				if (!requiredMethodsList.isEmpty()) 
					 throw new RequiredTraitMethodsException(loadedClass, requiredMethodsList);
				 

				/** Insert implemented Methods on loadedClass **/
				for (TraitMethod a : implementedMethodsList) {
					if (!a.getTraitClass().equals(loadedClass.getName())) {
						try {
							
							if(!ContainsDeclared(loadedClass, a.getAlias()))
							
								loadedClass.addMethod(CtNewMethod.copy(a.getMethod(), loadedClass, null));
								
						} catch (CannotCompileException e) {
							// TODO tratar exception
							e.printStackTrace();
						}

					}

				}

			}
			

		} catch (NotFoundException e) {
			e.printStackTrace();
			System.exit(1);
		} catch (RequiredTraitMethodsException e) {
			e.printRequiredTraitMethodsException();
		} catch (CannotCompileException e) {
			
			e.printStackTrace();
		}

	}

	/**
	 * This method aims to create the composition of Traits. It verifies the
	 * usage of the Trait and throws a BadUsageTraitException
	 * 
	 * @param loadedClass
	 * @return LoadedTrait
	 * @throws CannotCompileException 
	 */
	private LoadedTrait composeTrait(LoadedTrait loadedClass) throws CannotCompileException {

		try {
			boolean isAliased = false;
			
			Aliases aliasesList = new Aliases();
			
			CtClass superCt = loadedClass.getTrait().getSuperclass();

			/** Check Trait integrety **/
			if (!loadedClass.getTrait().getName().equals(this.mainClass))
				this.tools.checkTraitUsage(loadedClass.getTrait(), superCt);

			ArrayList<LoadedTrait> Traitsloaded = new ArrayList<LoadedTrait>();

			LoadedTrait tested;

			/**  check the usage os aliasedtrait annotation**/
			if(this.tools.isAliasedTraits(loadedClass.getTrait()))
				throw new BadUsageAliasedTraitException(loadedClass.getTrait());
				
			/** Check if Class is composed with other Traits **/
			if (this.tools.isWithTraits(loadedClass.getTrait())) {

				Class<?>[] traits = this.tools.getTraits(loadedClass.getTrait()
						.getAnnotations());

				for (Class<?> trait : traits) {

					/** Verify if exist loops **/
					verifyLoops(trait.getName());

					CtClass CtTrait = this.traitSystemPool.get(trait.getName());

					int verification = this.alreadyLoaded(CtTrait);

					/**
					 * Verifies if Trait already been loaded before There is no
					 * point to compose the same trait more than once
					 **/
					if (verification != -1)
						tested = this.loadedTraits.get(verification);

					else {
						tested = new LoadedTrait(CtTrait);
						tested = composeTrait(tested);
					}

					Traitsloaded.add(tested);
				}

				/** Compose withAliasedTraits **/
			} else if (this.tools.isWithAliasedTraits(loadedClass.getTrait())) {

				isAliased = true;
				
				AliasedTrait[] annotations = this.tools
						.getAliasedTraits(loadedClass.getTrait()
								.getAnnotations());

				for (AliasedTrait aliased : annotations) {

					CtClass CtTrait = this.traitSystemPool.get(aliased.trait()
							.getName());

					/** Verify if exist loops **/
					verifyLoops(CtTrait.getName());

					tested = new LoadedTrait(CtTrait, aliased.aliases());

					tested = composeTrait(tested);
					
					aliasesList.pushAliases(aliased.aliases());
					
					Traitsloaded.add(tested);

				}
			}

			/** Fill list of the Trait and joins with their subtraits (if any) **/
			loadedClass.fillMethodsList();

			/** Join subTraits **/
			if (!Traitsloaded.isEmpty())
				loadedClass.joinTraits(Traitsloaded);

			/** treat MethodCalls **/
			if(isAliased)
			
				loadedClass = treatMethodCalls(loadedClass, aliasesList);
			
			/** Check and treat if Trait as extends **/
			loadedClass = treatSuper(loadedClass, superCt);
			
			/** Add Trait to loadedTraits list **/
			this.loadedTraits.add(loadedClass);
			
		} catch (NotFoundException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (NotTraitException e) {
			e.printNotTraitExceptionDetailedMessage();
		} catch (MissingTraitAbstractKeyWordException e) {
			e.printMissingTraitAbstractKeyWordException();
		} catch (BadTraitMethodDefinitionException e) {
			e.printBadTraitMethodException();
		} catch (PublicFieldsOnTraitException e) {
			e.printPublicFieldsOnTraitException();
		} catch (BadUsageAliasedTraitException e) {
			e.printBadUsageAliasedTraitExceptionDetailedMessage();
		}

		return loadedClass;
	}

	/**
	 * This method check if the Trait was already Loaded on the loadedTraits list,
	 * before Only for non aliased traits
	 * @param trait
	 * @return i - the position of the trait on the list, -1 when not found
	 */
	private int alreadyLoaded(CtClass trait) {

		int i = 0;
		for (LoadedTrait loaded : this.loadedTraits) {

			if (loaded.getTrait().equals(trait) && !loaded.isAliaded())
				return i;
			i++;
		}
		return -1;
	}

	/**
	 * This method is used to discover Traits that have a loop in their
	 * composition.
	 * @param trait
	 */
	private void verifyLoops(String trait) {

		if (isLoop(trait)) {
			System.err.println("Loop in Traits including " + trait);
			System.exit(1);
		} else
			this.passedTraits.add(trait);

	}

	/**
	 * This method verify whether the trait is loaded or not by
	 * searching for its name on the loadedTraits list
	 * @param trait
	 * @return true - if the trait is there, false otherwise
	 */
	private boolean loaded(String trait) {

		for (LoadedTrait test : this.loadedTraits)
			if (test.getTrait().getName().equals(trait))
				return true;

		return false;
	}

	/**
	 * This method verify whether the trait is loaded and is 
	 * constituting a loop
	 * @param trait
	 * @return true - if there's a loop, false otherwise
	 */
	private boolean isLoop(String trait) {

		if (alreadyPassed(trait) && !loaded(trait))
			return true;
		else
			return false;
	}

	/**
	 * This method searches for the trait on the passedTraits list
	 * @param trait
	 * @return true - when found, else returns false
	 */
	private boolean alreadyPassed(String trait) {

		for (String test : this.passedTraits)
			if (test.equals(trait))
				return true;
		
		return false;
	}
	
	public String inInterface(String name, String traitname) throws NotFoundException{
		
		CtClass trait = this.traitSystemPool.get(traitname);
		for(CtClass inter : trait.getInterfaces())
			for(CtMethod method : inter.getDeclaredMethods())
				if(method.getName().equals(name))
					return inter.getName();
		
		return null;
	}
	
	private boolean ContainsDeclared(CtClass ct, String name){
		
		for(CtMethod a : ct.getDeclaredMethods())
			if(a.getName().equals(name))
				return true;
		
		return false;
		
	}
	
	private String getTemplate(CtMethod method) throws NotFoundException{
		
		String template;
		
		if(method.getParameterTypes().length == 0 
				&& method.getReturnType().getName().equals("void")){
			template = "{ this.%s();}";
		}else if (method.getParameterTypes().length > 0 
				&& method.getReturnType().getName().equals("void")){
			template =  "{ this.%s($$);}";
		}else if (method.getParameterTypes().length > 0 
				&& !method.getReturnType().getName().equals("void")){
			template = "{ $_ = this.%s($$);}";
		} else{
			template = "{ $_ = this.%s();}";
		}
		
		return template;
	}
	
	
	private LoadedTrait treatMethodCalls(LoadedTrait loadedClass, Aliases aliasesList) 
			throws CannotCompileException, NotFoundException{
		
		CtClass trait = loadedClass.getTrait();
		int i;
		for(i=0; i < aliasesList.lenght(); i+=2){
			
			final String neww = aliasesList.get(i+1);
			final String old = aliasesList.get(i);
			
			TraitMethod tm = loadedClass.getImplementedMethod(neww);
			CtMethod traitmethod = tm.getMethod();
			traitmethod.setName(neww);
			trait.addMethod(CtNewMethod.copy(traitmethod, trait, null));
			
			final String template = getTemplate(traitmethod);
			final String interfac = inInterface(old, tm.getTraitClass());
			
			/** change method call **/
			trait.instrument(
				    new ExprEditor() {
				        public void edit(MethodCall m)
				                      throws CannotCompileException
				        {
				            if (m.getClassName().equals(interfac)
				                          && m.getMethodName().equals(old))
				                m.replace(String.format(template, neww));
				            }
				    });
			
			/** update methodlist **/
			loadedClass.updateList(trait);
	
		}
		
		return loadedClass;
	}
	
	private LoadedTrait treatSuper(LoadedTrait loadedClass, CtClass superCt) 
				throws CannotCompileException, NotFoundException{
		
		LoadedTrait tested;
		
		if (!superCt.getName().equals("java.lang.Object")) {
			
			int verification = this.alreadyLoaded(superCt);

			/**
			 * Verifies if Trait already been loaded before There is no
			 * point to compose the same trait more than once
			 **/
			if (verification != -1)
				tested = this.loadedTraits.get(verification);
			else {
				tested = new LoadedTrait(superCt);
				tested = composeTrait(tested);
			}

			loadedClass.joinSuper(tested);
		}
		
		return loadedClass;
		
	}
	
}
